package com.sunday.common.mq.rabbit.study.base.e19_HandlingUnroutableMessages;

import com.rabbitmq.client.*;
import com.rabbitmq.client.impl.DefaultExceptionHandler;
import com.sunday.common.mq.rabbit.study.base.ConnectionUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/**
 * 处理不可路由的消息
 * 如果发布消息时设置了“mandatory”标志， 但无法路由，经纪人会将其返回给 发送客户端（通过 AMQP。基本返回命令）。
 * 要收到此类返回的通知，客户端可以实现 ReturnListener 接口并调用 Channel.addReturnListener。 如果客户端尚未为特定通道配置返回侦听器， 然后，关联的返回消息将被静默丢弃。
 * <p>
 * 例如，如果客户端发布的消息的“mandatory”标志设置为未绑定到队列的“direct”类型的交换，则会调用返回监听器。
 * <p>
 * 触发条件：
 * 1.创建了exchange但是没有为他绑定队列导致的消息未到达合适队列
 * 2.创建了exchange同时创建了queue，但是在将两者绑定的时候，
 * 使用的bindingKey和消息发布者使用的rountingKey不一致导致的消息未到达合适队列
 * <p>
 * RabbitMQ中监听器有ReturnListener、ConfirmListener、ShutdownListener，
 * 本练习中使用ReturnListener，在发布消息时设置mandatory等于true，
 * 监听消息是否有相匹配的队列，没有时ReturnListener将执行handleReturn方法，消息将返给发送者
 * 设置mandatory=true，当路由不到队列时返回给消息发送者，在return监听器中接收
 *
 * 触发条件：
 * 1.创建了exchange但是没有为他绑定队列导致的消息未到达合适队列
 * 2.创建了exchange同时创建了queue，但是在将两者绑定的时候，
 * 使用的bindingKey和消息发布者使用的routingKey不一致导致的消息未到达合适队列
 * 反正就是要有exchange 然后根据 routingKey 获取不到 queue就会被 addReturnListener捕获
 */
@Slf4j
public class HandlingUnroutableMessages {

    /**
     * 在 RabbitMQ 中，ch.confirmSelect();和设置 “mandatory” 标志有以下区别：
     * 一、ch.confirmSelect();
     * 功能描述：
     * 启用发布确认模式。当使用这个方法后，生产者发送的消息会得到 RabbitMQ 服务器的确认响应。
     * 这意味着生产者可以知道消息是否成功被 RabbitMQ 接收并持久化到磁盘（如果配置了持久化）。
     * 确认机制：
     * 生产者在发送消息后，可以等待来自 RabbitMQ 服务器的确认信号。如果收到确认，表明消息已被正确处理；如果收到否定确认或者超时未收到确认，生产者可以采取相应的措施，如重新发送消息。
     * 适用场景：
     * 对于需要确保消息可靠传递的场景非常有用。例如，在金融交易系统中，确保每一笔交易消息都被正确处理至关重要，使用发布确认模式可以提供更高的可靠性保证。
     * 二、“mandatory” 标志
     * 功能描述：
     * 当在发布消息时设置 “mandatory” 标志为true，如果消息无法路由到任何队列，RabbitMQ 会将消息返回给生产者，并触发一个回调函数。
     * 如果 “mandatory” 标志为false（默认值），消息无法路由时会被直接丢弃。
     * 处理方式：
     * 主要用于处理消息路由失败的情况。生产者可以根据返回的消息或者回调函数中的信息进行相应的处理，例如记录日志、重新发送消息或者采取其他补偿措施。
     * 适用场景：
     * 当生产者需要知道消息是否成功路由到队列时，可以使用 “mandatory” 标志。例如，在一个分布式系统中，生产者可能需要确保消息被正确地分发到各个处理节点对应的队列中，如果消息无法路由，生产者可以采取相应的措施来调整系统的配置或重新发送消息。
     * 综上所述，ch.confirmSelect();主要用于确保消息被 RabbitMQ 服务器接收和处理，而 “mandatory” 标志主要用于处理消息无法路由到队列的情况。它们在不同的场景下为生产者提供了不同的可靠性保障机制。
     */

    @Test
    @SneakyThrows
    public void returnListener() {

        String EXCHANGE_NAME = "exchange_listener";
        String ROUTING_KEY_NAME = "unknown";

        log.info(" [x] start ");

        ConnectionFactory cf = ConnectionUtil.factory();
        /**
         * 未处理的异常
         * 与连接、通道、恢复、 和使用者生命周期委托给例外 处理器。异常处理程序是实现异常处理程序接口的任何对象。
         * 默认情况下，一个 使用 DefaultExceptionHandler 的实例。它 将异常详细信息打印到标准输出。
         * 可以使用 ConnectionFactory#setExceptionHandler 覆盖处理程序。
         */
        cf.setExceptionHandler(new DefaultExceptionHandler());

        try (Connection connection = cf.newConnection()) {
            Channel channel = connection.createChannel();

            channel.exchangeDelete(EXCHANGE_NAME);

            /**
             * addShutdownListener（ShutdownListener listener）和
             * removeShutdownListener（ShutdownListener listener），
             * 用于管理任何侦听器，当对象转换为关闭状态时将触发这些侦听器
             * 。请注意，将ShutdownListener添加到已关闭的对象将立即触发侦听器
             *
             * 关断协议
             * 客户端关闭过程概述
             * AMQP 0-9-1 连接和通道共享相同的常规 管理网络故障、内部故障的方法、 和显式本地关闭。
             * AMQP 0-9-1 连接和通道具有以下生命周期状态：
             * 打开：对象已准备就绪，可供使用
             * 关闭：对象已显式 通知本地关闭，已发出关闭 请求任何支持的下层对象，并且 等待其关闭程序完成
             * 已关闭：对象已收到全部 来自任何较低层的关机完成通知 对象，并因此自行关闭
             * 这些对象总是以关闭状态结束， 无论导致关闭的原因是什么，例如 应用程序请求，内部客户端库 故障、远程网络请求或网络故障。
             * 连接和通道对象具有 以下与关机相关的方法：
             * addShutdownListener（ShutdownListener listener） 和
             * removeShutdownListener（ShutdownListener listener）），以管理任何侦听器，这将 在对象转换到关闭状态时触发。请注意，添加 关闭已关闭对象的侦听器 会立即解雇侦听器
             * getCloseReason（）， 以允许 调查物体的原因是什么 关闭
             * isOpen（），用于测试是否 对象处于打开状态
             * close（int closeCode， String closeMessage），用于显式通知对象 要关闭
             */
            // clean connection shutdown; protocol method: #method<connection.close>(reply-code=200, reply-text=OK, class-id=0, method-id=0)
            connection.addShutdownListener((cause) -> log.error(" [x] shutdownCompleted cause : {} ", cause.toString()));

            /**
             * 如果发布的消息设置了“强制”标志，但无法路由，则代理会将其返回给发送客户端（通过AMQP.Basic.Return 命令）。
             * 通知这样的回报，客户可以实现ReturnListener
             * 接口并调用Channel.addReturnListener。如果客户端尚未为特定通道配置返回侦听器，
             * 则将以静默方式删除关联的返回消息。 void
             * com.rabbitmq.client.Channel.basicPublish(String exchange, String
             * routingKey, boolean mandatory, boolean immediate, BasicProperties
             * props, byte[] body) throws IOException
             * 例如，如果客户端发布一条消息，其中“mandatory”标志设置为未绑定到队列的“direct”类型的交换，则将调用返回侦听器。
             */
            channel.addReturnListener((replyCode, replyText, exchange, routingKey, properties, body) -> {
                log.error(" [x] replyCode : '{}' ", replyCode);
                log.error(" [x] replyText : '{}' ", replyText);
                log.error(" [x] exchange : '{}' ", exchange);
                log.error(" [x] routingKey : '{}' ", routingKey);
                log.error(" [x] properties : '{}' ", properties);
                log.error(" [x] body : '{}' ", new String(body, "UTF-8"));
            });

            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

            for (int i = 0; i < 1; i++) {
                channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY_NAME, true, false, null, String.valueOf(i).getBytes(StandardCharsets.UTF_8));
            }

            TimeUnit.SECONDS.sleep(2);

        } catch (Exception e) {

            log.error(e.toString(), e);
            /**
             * AMQP连接和通道对象具有以下与关闭相关的方法：
             *
             * addShutdownListener（ShutdownListener listener）和
             * removeShutdownListener（ShutdownListener listener），用于管理任何侦听器，当对象转换为关闭状态时将触发这些侦听器 。
             * 请注意，将ShutdownListener添加到已关闭的对象将立即触发侦听器
             * getCloseReason（），允许调查对象关闭的原因是什么 isOpen（），用于测试对象是否处于打开状态 close（int
             * closeCode，String closeMessage），显式通知对象关闭
             */
        }

    }



}
